DNN_Classification.py

### Copyright (C) 2020-2023  Alessio Gianelle, INFN Padova
###
### This program is free software: you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation, either version 3 of the License, or
### later version.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
###
### You should found a copy of the GNU General Public License
### at this path: https://www.gnu.org/licenses/ .

# coding: utf-8
import sys
import os
import gc
# import keras
from   keras.models          import Model
from   keras.callbacks       import ModelCheckpoint, EarlyStopping
from   keras.layers          import Input
from keras.layers.normalization import BatchNormalization
from   keras                 import optimizers
from keras.optimizers import SGD
from keras.constraints import maxnorm
# import sklearn
from sklearn.metrics         import confusion_matrix, roc_curve, auc, classification_report, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from keras.layers import Dense, Dropout
from keras.wrappers.scikit_learn import KerasClassifier
# extra
import pandas as pd
import numpy as np
import tensorflow
from joblib import dump, load


# Our utilities...
from Utilities import *

### These to limit the GPU memory resources used by keras ###

import tensorflow as tf

# TensorFlow wizardry for GPU dynamic memory allocation
config = tf.compat.v1.ConfigProto()

# Don't pre-allocate memory; allocate as-needed
config.gpu_options.allow_growth = True

# Only allow a fraction of the GPU memory to be allocated
config.gpu_options.per_process_gpu_memory_fraction = 0.5

# Create a session with the above options specified.
tf.compat.v1.keras.backend.set_session(tf.compat.v1.Session(config=config))

#############################################################


# Remove warning messages from commands output
def warn(*args, **kwargs):
    pass

import warnings
warnings.warn = warn

applyPCA = False
thrPCA = 0.98
verb = 1

# first argoment is the configuration file
conf = __import__(sys.argv[1])

testpath = conf.testpath

selectfeatures = conf.features
# if the second argoment is set used reduced features
if sys.argv[2] == 'red':
  selectfeatures = conf.reducedFeatures
  testpath += "Red"
elif sys.argv[2] == 'imp':
  selectfeatures = conf.importanceFeatures
  testpath += "IF"

# create test directory
if not os.path.exists(testpath):
    os.makedirs(testpath)

basepath = "%s/%s"%(conf.datapath, conf.datadir)

for pa in conf.namePopA:

  rawA = pd.read_hdf("%s/Ca_%s.hdf"%(basepath,pa))
  popA = rawA[selectfeatures]
  popA['label'] = 0

  ndata = popA.shape[0]

  for pb in conf.namePopB:
    rawB = pd.read_hdf("%s/Ma_%s.hdf"%(basepath,pb))
    popB = rawB[selectfeatures]
    popB['label'] = 1

    classname = [pa, pb]

    name="%s-%s"%(tuple(classname))
    pmsg(0, verb, "Study %s (%d) vs %s (%d)"%(classname[0], popA.shape[0], classname[1], popB.shape[0]))

    data, label = prepareData(popA, popB, applyPCA, thrPCA, scaleData=False, shuffleData=False, equal=True)

    nfeat=data.shape[1]

    rawXtrain, Xtestr, rawytrain, ytest = train_test_split(data, label, random_state=17)
    Xtrainr, Xvalidr, ytrain, yvalid = train_test_split(rawXtrain, rawytrain, random_state=17)

    pmsg(2, verb, "Use %d samples for trainig, %d for evaluation and %d for test"%(Xtrainr.shape[0], Xtestr.shape[0], Xvalidr.shape[0]))

    scaler = StandardScaler()
    Xtrain = scaler.fit_transform(Xtrainr)

    Xvalid = scaler.transform(Xvalidr)
    Xtest = scaler.transform(Xtestr)

    # Save scaler model 
    dump(scaler, '%s/Scaler_%s.gz'%(testpath, name), compress=('gzip', 3))

    nfeat = data.shape[1]

    def create_model(kinit='glorot_uniform', act='relu', dropRate=0.1, opt='SGD', lr=0.01, mnt=0.0, weight_constraint=4, bnm=0.1, bne=1e-06 ):
      modIn  = Input(shape=(nfeat,))
      modOut = Dense(350, kernel_initializer=kinit, activation=act, kernel_constraint=maxnorm(weight_constraint))(modIn)
      modOut = BatchNormalization(momentum=bnm, epsilon=bne)(modOut)
      modOut = Dropout(dropRate)(modOut)
      modOut = Dense(224, kernel_initializer=kinit, activation=act, kernel_constraint=maxnorm(weight_constraint))(modOut)
      #modOut = BatchNormalization(momentum=bnm, epsilon=bne)(modOut)
      modOut = Dropout(dropRate)(modOut)
      modOut = Dense(128, kernel_initializer=kinit, activation=act, kernel_constraint=maxnorm(weight_constraint))(modOut)
      #modOut = BatchNormalization(momentum=bnm, epsilon=bne)(modOut)
      modOut = Dropout(dropRate)(modOut)
      modOut = Dense(96, kernel_initializer=kinit, activation=act, kernel_constraint=maxnorm(weight_constraint))(modOut)
      #modOut = BatchNormalization(momentum=bnm, epsilon=bne)(modOut)
      modOut = Dropout(dropRate)(modOut)
      modOut = Dense(96, kernel_initializer=kinit, activation=act, kernel_constraint=maxnorm(weight_constraint))(modOut)
      #modOut = BatchNormalization(momentum=bnm, epsilon=bne)(modOut)
      modOut = Dropout(dropRate)(modOut)
      modOut = Dense(1, activation='sigmoid', kernel_initializer=kinit)(modOut)
      DNN = Model(inputs=modIn, outputs=modOut)
      optim = optimizers.Adam(lr=lr)
      if opt == 'Adam':
        optim = optimizers.Adam(lr=lr)
      if opt == 'SGD':
        optim = optimizers.SGD(lr=lr, momentum=mnt)
      if opt == 'Adamx':
        optim = optimizers.Adamx(lr=lr)
      DNN.compile(loss='binary_crossentropy', optimizer=optim, metrics=['accuracy'])
      return DNN

    model = create_model()

    checkpointer = ModelCheckpoint(filepath="%s/Model_%s.h5"%(testpath, name), verbose=1, save_best_only=True)
    earlystop = EarlyStopping(monitor='val_loss', min_delta=0.0001, patience=25, verbose=0, mode='auto')

    hist = model.fit(Xtrain, ytrain, epochs=250, batch_size=64, validation_data=(Xvalid, yvalid), verbose=0, callbacks=[checkpointer, earlystop])

    model.load_weights("%s/Model_%s.h5"%(testpath, name))
    scaler = load('%s/Scaler_%s.gz'%(testpath, name))

    score = model.evaluate(Xtest, ytest, verbose=0)
    pmsg(0, verb, 'Test validation score: %f'%score[0])
    pmsg(0, verb, 'Test validation accuracy: %f'%score[1])

    testProb = model.predict(Xtest, verbose=0)
    testClasses =  np.array([round(x[0]) for x in testProb], dtype='int64')

    # Confusion matrix for test
    confmatrix(confusion_matrix(ytest, testClasses), classname, "%s/ConfMatrix_%s.png"%(testpath, name))
    pmsg(1, verb, classification_report(ytest, testClasses, target_names=classname))

    fpr, tpr, _ = roc_curve(ytest, testProb)
    roc_auc = auc(fpr, tpr)

    # Compute macro-average ROC curve and ROC area
    plotroc(fpr, tpr, roc_auc, filename="%s/ROC_%s.png"%(testpath, name))

    pmsg(0, verb, "Roc auc score: %f"%roc_auc_score(ytest, testProb))
                                                                                                    
    Ar, la = prepareData(popA, pd.DataFrame(), applyPCA, thrPCA, scaleData=False, shuffleData=False)
    Br, lb = prepareData(pd.DataFrame(), popB, applyPCA, thrPCA, scaleData=False, shuffleData=False)
    A = scaler.transform(Ar)
    B = scaler.transform(Br)
    testProbA = model.predict(A)
    testProbB = model.predict(B)
    bins = np.linspace(0, 1, 100)
    plt.hist(testProbA, bins, alpha=0.5, label=classname[0])
    plt.hist(testProbB, bins, alpha=0.5, label=classname[1])
    plt.legend(loc='upper right')
    showplot("%s/ProbDist_%s.png"%(testpath, name))

    history(hist, "%s/Hist_%s.png"%(testpath, name))

    resA = pd.concat([rawA, pd.DataFrame(1-testProbA, columns=['probabilities'])], axis=1)
    resB = pd.concat([rawB, pd.DataFrame(testProbB, columns=['probabilities'])], axis=1)

    with pd.ExcelWriter("%s/Res_%s.xlsx"%(testpath, name)) as writer:
      resA.to_excel(writer, sheet_name='%s'%classname[0])
      resB.to_excel(writer, sheet_name='%s'%classname[1])

plt.close('all')
pmsg(0, 0, "\nThat is all!")



